(ns com.negoflo.twitter
  (:use clojure.xml [clojure.contrib.str-utils :only (str-join)])
  (:import (java.net URL URLConnection URLEncoder)
	   (java.io BufferedReader InputStreamReader OutputStreamWriter)
	   (java.util Date)
	   (java.text SimpleDateFormat DateFormat FieldPosition)))

(def encode-table "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")

(defn- encode-num 
  [num]
      (let [a (bit-and num 63)
	    b (bit-shift-right (bit-and num 4032) 6)
	    c (bit-shift-right (bit-and num 258048) 12)
	    d (bit-shift-right (bit-and num 16515072) 18)]
	(map (fn [x]  (nth encode-table x )) (list d c b a))))

(defn- pack-bits [bytes]
  (+ (* 256 256 (nth bytes 0)) (* 256 (nth bytes 1)) (nth bytes 2)))

(defn- padding [ints]
  (let [ints-zero-pad (take 3 (concat ints (repeat 0)))]
      (take 4 (concat (take (+ (count ints) 1) 
			    (encode-num (pack-bits ints-zero-pad))) 
		      (repeat \=)))))

(defn- encode [str-in]
  (loop [acc "" s str-in]
    (let [x (map int (take 3 s))]
      (if  (< (count s) 4) 
	(concat acc (padding x))
	(recur (concat acc (encode-num (pack-bits x))) (drop 3 s)))))) 

(defn base64-encode [str-in]
  (apply str (encode str-in)))

(defn- connection [url-str & [username password post]]
  "Open connection to twitter. Passes auth info."
  (let [url (new URL url-str)
	conn (. url openConnection)
	encoded (base64-encode (str username ":" password))]
    (do (when post (. conn setDoOutput true))
	(when (and username password)
	  (doto conn (.setRequestProperty "Authorization" 
					  (str "Basic " encoded)))) conn)))

(defn- make-nice-tag [twt-map]
  "Further parsing of XML into cleaner map representation."
  (let [fields (map (fn [tag] { (:tag tag)
				(let [content (:content tag)
				      val (first content)]
				  (if (> (count content) 1) (make-nice-tag content)
				   (if (not (= nil val)) 
				     (cond (re-matches #"[0-9]+" val) (bigint val)
					   (re-matches #"true" val) true
					   (re-matches #"false" val) false
					   :else val))))
				}) twt-map)]
    (reduce conj {} fields)))

(defn- make-tweet-list [parsed-xml]
  "Takes parsed XML and creates list of maps, each map representing a status."
  (map #(make-nice-tag (:content %)) (:content parsed-xml)))

(defn index-tweets [tweets]
  "Produces map of tweets indexed by id."
  (reduce conj {} (map (fn [twt] {(:id twt) twt}) tweets)))

(defn find-by-id [id indexed-tweets]
  (first (rest (find indexed-tweets id))))

(def base_url "http://twitter.com/")

(defn- format-date [#^java.util.Date date]
  (if (not (nil? date))
    (let [str-buf (new StringBuffer)
	  dateFormat (new SimpleDateFormat "EEE, dd MMM yyyy HH:mm:ss z")
	  pos (new FieldPosition 0)]
      (. URLEncoder encode (. (. dateFormat format date str-buf pos) toString)))))

(defn- make-opt-str [options]
  (map (fn [[k v]] (str (name k) "=" 
		      (if (isa? (class v) java.util.Date)
			(format-date v) v))) options))

(defn- encode-opts [option-map]
      (let [options (filter (fn [[k v]] (not= v nil)) option-map)]
	(apply str "?" (str-join "&" (make-opt-str options)))))

(defn- get-http-method
  [username password op-url opts-map]
  (let [url (str base_url op-url (encode-opts opts-map))
	stream (. (connection url username password) getInputStream)
	statuses (make-tweet-list (parse stream))]
	 (do (. stream close) statuses)))

(defmacro map-params-
  ([a-map x] `(assoc ~a-map ~(keyword (name x))  ~x))
  ([a-map x & more] `(map-params- (assoc ~a-map ~(keyword (name x)) ~x) ~@more)))

(defmacro map-params [& args]
  `(map-params- {} ~@args))

(defmacro defmeth [fname path doc user pass & args]
 `(defn ~fname doc [~user ~pass & [~@args]]
    (get-http-method ~user ~pass 
		~(str (name path) "/" (name fname) ".xml")
		(map-params ~@args))))

(defmacro defmeth_noauth [fname path doc & args]
  `(defmeth ~fname ~path ~doc x# y# ~@args))

;; Status Methods

(defn public_timeline
  "Returns the 20 most recent statuses from non-protected users who
  have set a custom user icon."
  []
  (get-http-method nil nil "statuses/public_timeline.xml" nil))

(defmeth friends_timeline statuses
  "Returns the 20 most recent statuses posted by the authenticating
  user and that user's friends. The since argument must be a Date
  object. The rest are integers."
  username password
  since since_id count page)

(defmeth user_timeline statuses
  "Returns the 20 most recent statuses posted from the authenticating
  user."
  username password
  since since_id count page)

(defn show 
  "Returns a single status, specified by the id parameter below.  The
  status's author will be returned inline."
  [id]
 (get-http-method nil nil (str "statuses/show/" id ".xml") nil))

(defn update
  "Updates the authenticating user's status.  Requires the status
  parameter specified below. A status update with text identical to
  the authenticating user's current status will be ignored."
  [username password status & [in_reply_to_status_id]]
  (let [url (str base_url "statuses/update.xml") 
  	data (encode-opts {:status status :in_reply_to_status_id in_reply_to_status_id})
	conn (connection url username password true)
	out-stream (new OutputStreamWriter (. conn getOutputStream))]
	(do (doto out-stream (.write data) (.flush))
	    (let [in-stream (. conn getInputStream)
		  result (make-nice-tag (:content (parse in-stream)))]
	      (do (. in-stream close) result)))))

(defmeth replies statuses
  "Returns the 20 most recent @replies (status updates prefixed with
  @username) for the authenticating user."
  username password 
  page since since_id)

(defn replies
  "Returns the 20 most recent @replies (status updates prefixed with
  @username) for the authenticating user."
  [username password & [page since since_id]]
  (get-http-method username password "statuses/replies.xml"
	      {:page page :since since :since_id since_id}))

(defmeth mentions statuses
  "Returns the 20 most recent mentions (status containing @username)
  for the authenticating user."
  username password
  since_id max_id count page)

(defmeth_noauth friends statuses
  "Returns a user's friends, each with current status inline."
  id user_id screen_name page)

(defmeth_noauth ids followers
  "Returns an array of numeric IDs for every user following the specified user."
  id page)